#!/usr/bin/python
#-*-coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

u"""
Book : an improved dict handling datum objects
"""

__all__ = [
           u'Book',
           u'GroupBook'
           ]

import copy

#from pyLot.recording import recordable

from _pylot.minimal.datum import Datum
from _pylot.minimal.path import Path
from _pylot.minimal.system import ENCODING
from _pylot.minimal.utils import (Utils, is_iterable, check_type, check_value,
     unicode2, uprint, error)
from _pylot.data import regexpr

from _pylot.legacy.identity import Identity

class Book (dict) :
  u"""
  An improved `dict` designed for Datum handling.

  Book is a dictionary specialized in book

  >>> from pyLot.core import Book
  >>> book = Book(Datum(u'd1', value=1, group=u'g1'))
  >>> book = Book(Datum(u'one', int, 1), Datum(u'two', unicode, u'deux'))
  >>> book.keys()
  [u'two', u'one']
  """
  datum_type = Datum
  identity = Identity()

  def __init__ (self, *t, **d):
    u"""
    >>> b = Book(title = u'NouveauLivre')
    >>> b
    Book(,
      identity=Identity(name=u'NouveauLivre', nature=None, tooltip=None, group=None, alias=None))
    >>> b = Book()
    >>> b.identity
    Identity(name=None, nature=None, tooltip=None, group=None, alias=None)

    .. seealso ::

      Book specifications
    """
    super(Book, self).__init__()
    self._datum_type = Datum

    if u'identity' in d :
      self.identity = d.pop(u'identity')
      try :
        assert (type(self.identity) == Identity)
      except AssertionError :
        self.identity = Identity()
    else :
      self.identity = Identity()

    if u'title' in d :
      self.identity.name = d.pop(u'title')
    if u'tooltip' in d :
      self.identity.tooltip = d.pop(u'tooltip')
    if u'datum_type' in d :
      self._datum_type = d.pop(u'datum_type')
    else :
      self._datum_type = Datum
    self.datum_type = self._datum_type
    self._up(*t, **d)

  def userUpgrade(self, book):
    self.upgrade(book,
      mod_attrs=[u'value', u'default_value', u'value_list'],
      only_existing_keys=True)

  def upgrade (self, book, mod_attrs=None, strict=True,
               only_existing_keys=False):
    u"""
    Upgrades do a Datum update for all Datum yet in Book
    Else add datum

    :param book: the updater
    :type book: Book or dict of Datum or dict of value
    """

    check_type(u'book', book, dict)
    check_type(u'mod_attrs', mod_attrs, [list, tuple], none=True)
    check_type(u'only_existing_keys', only_existing_keys, bool)
    check_type(u'strict', strict, bool)
    for key, value in book.items() :
      if isinstance(value, self._datum_type) :
        datum = value
        if strict : #raise error if datum nature differ from book nature
          self._checkDatumNature(datum)
        else : #if book.nature is defined, try to convert datum to this nature
          if self.identity.nature :
            datum.nature = self.identity.nature
      else :
        if isinstance(key, str) :
          key = key.decode(ENCODING)
        datum = self._datum_type(key, self.identity.nature, value=value)

      if key in self :
        self[key].update(datum, mod_attrs)
      elif only_existing_keys is False :
        self[key] = datum
      else :
        pass

  def extract(self, **d):
    u"""
    Extracts from Book all Datum whose field attribute matches value

    Example :

    >>> from pyLot.core import Datum, Path
    >>> book = Book()
    >>> book.upFromDatum(Datum(u'PATH1', Path, u'/usr/bin', group=u'pathgroup'))
    >>> book.upFromDatum(Datum(u'INT', int, 1, 0))
    >>> book.upFromDatum(Datum(u'INT2', int, 1))
    >>> book.upFromDatum(Datum(u'PREFIX', Path, u'/usr/local', group=u'pathgroup'))
    >>> lst = book.extract(group=u'pathgroup').keys() ; lst.sort() ; lst
    [u'PATH1', u'PREFIX']
    >>> lst = book.extract(nature=int).keys() ; lst.sort() ; lst
    [u'INT', u'INT2']

    :param d: field=value. Ex: group='gr1'

    :return: a Book with Datum satisfying datum.group==group
    :rtype: Book

    """
    field = None
    for key in d :
      if key in self._datum_type.ordered_keys :
        field = key
        value = d[key]
        break

    if field :
      book = Book()
      for datum in self.values():
        if getattr (datum, field) == value :
          book[datum.name] = datum
    else:
      book = self
    return book

  def extract2 (self, field=u'value', **d):
    u"""
    Like extract but return list of datum 'field' instead of book

    .. warning ::
    
      not optimized method !
    """
    check_type(u'field', field, unicode)
    return [getattr(d, field) for d in self.extract(**d).values()]

  def resetDataToDefault(self):
    u"""
    Sets every data to default_value
    """
    for datum in self.values() :
      if datum.default_value is not None :
        datum.value = datum.default_value

  def add (self, datum):
    u"""
    Adds datum to book
    
    :param datum: datum you want to add
    :type datum: :class:`~pylot.core.Datum`
    """
    check_type(u'datum', datum, Datum)
    self._up(datum)

  def _up(self, *t, **d):
    u"""
    This method updates book.
    See book specification and constructor documentation for further 
    information about function arguments. 
    """
    if u'mod_attrs' in d :
      mod_attrs = d.pop(u'mod_attrs')
    else :
      mod_attrs = Datum.getFieldsAll()

    if len(t) == 0: #tous les arg sont nommés
      self.upgrade(d, mod_attrs=mod_attrs)
    else:
      for elem in t :
        if isinstance(elem, self._datum_type) :
          #t = [Datum, Datum, ...]
          self.upgrade({elem.name:elem}, mod_attrs=mod_attrs)
        elif isinstance(elem, Book) :
          self.upgrade(elem, mod_attrs=mod_attrs)
        elif type(elem) == dict :
          #t = {'a':1}, {'x':3, 'y':4}
          self.upgrade(elem, mod_attrs=mod_attrs)
        elif is_iterable(elem) :
          #t = [('a', 1), ('b',2)]
          self.upgrade({elem[0]:elem[1]}, mod_attrs=mod_attrs)
        elif isinstance(elem, Path) :
          self.upFromFile(elem, mod_attrs=mod_attrs)


  def __setitem__ (self, key, datum):
    u"""
    Like dict __setitem__ but checks datum and transforms simple value to Datum
    If self.identity.nature is set, all datum must have same nature

    Example:

    >>> b = Book(identity = Identity(u'option', bool, alias=u'Option Book'))
    >>> b[u'un_Booleen'] = Datum(u'un_Booleen', bool, True)
    >>> b[u'test'] = 0

    >>> print b
    option (Option Book) type = <type 'bool'>:
      'test' (no alias) : False, <type 'bool'>
      'un_Booleen' (no alias) : True, <type 'bool'>

    >>> b = Book()
    >>> b[u'a'] = 1
    >>> print b[u'a']
    Datum(u'a', int, 1)

    :param key: key entry
    :type key: str

    :param datum: datum or value
    :type datum: datum or type supported by datum
    """
    if not isinstance(datum, self._datum_type) :
      if key in self :
        self[key].value = datum
      else :
        datum = self._datum_type(name=key, nature=self.identity.nature,
                                value=datum)
        self._checkDatumNature(datum)
        super(Book, self).__setitem__(key, datum)#methode de dict
    else:
      self._checkDatumName(key, datum)
      self._checkDatumNature(datum)
      super(Book, self).__setitem__(key, datum)#methode de dict

  def __deepcopy__ (self, memo=None, _nil=[]):
    newbook = self.__class__(identity=self.identity,
                             datum_type=self._datum_type)
    for datum in self.values() :
      newbook[datum.name] = copy.deepcopy(datum)
    return newbook

  def __unicode__(self):
    u"""
    >>> b = Book(Datum(u'd2'), Datum(u'd3', value=1.0),
    ... identity = Identity(u'MyBook', alias=u'mon livre'))
    >>> print b
    MyBook (mon livre) type = None:
      'd2' (no alias) : None, None
      'd3' (no alias) : 1.0, <type 'float'>
    """
    if self.identity == Identity() :
      ustr = u"Anonymous Book :\n"
    else:
      alias = self.identity.alias
      if alias is None :
        alias = u'Not aliased book'
      ustr = u'%s (%s) type = %s:\n' % (self.identity.name, alias,
        self.identity.nature)
    for key, datum in self.items():
      if datum.alias is None:
        alias = u'no alias'
      else:
        alias = datum.alias
      ustr = ustr + u'  %r (%s) : %s, %s\n' % (key.encode(ENCODING),
        alias, datum.value, datum.nature)
    return ustr.strip(u' ,\n')

  def __str__ (self):
    return unicode(self).encode(ENCODING)

  def __repr__(self):
    u"""
    >>> book = Book(Datum(u'd2'), Datum(u'd3', value=1.0),
    ... identity=Identity(u'MyBook'))
    >>> book
    Book(Datum(u'd2', None, None, None, None, None, None, None),
      Datum(u'd3', float, 1.0, None, [1.0], None, None, None),
      identity=Identity(name=u'MyBook', nature=None, tooltip=None, group=None, alias=None))
    """
    string = u'Book('
    for value in self.values():
      string = string + repr(value) + u',\n  '
    string = string.strip(u' ,\n')
    if self.identity == Identity():
      book_id = u''
    else:
      book_id = u',\n  identity=' + repr(self.identity)
    string = string + book_id
    return string.strip() + u')'

  def __call__ (self, name=None, group=None) :
    u"""
    returns value corresponding to name entry

    >>> book = Book(Datum(u'INT', int, 1))
    >>> print book(u'INT')
    1
    """
    if name :
      return self[name].value
    elif group :
      return self.extract(group=group)
    else :
      return self

  def __eq__ (self, other):
    u"""
    >>> b1 = Book(Datum(u'd1', int, 1), Datum(u'd2', float, 3.14))
    >>> b2 = Book(Datum(u'd1', int, 1), Datum(u'd2', float, 3.14))
    >>> print u'1:', b1 is b2, b1 == b2
    1: False True
    >>> b2.title = u'newtitle'
    >>> print u'2:', b1 is b2, b1 == b2
    2: False False
    >>> b2 = Book(Datum(u'd1', int, 2), Datum(u'd2', float, 3.14))
    >>> print u'3:', b1 is b2, b1 == b2
    3: False False
    """
    assert(type(self) == type(other))
    if not self.identity == other.identity :
      return False
    for datum in self :
      if self[datum] != other[datum] :
        return False
    return True

  def __ne__ (self, other):
    return not self.__eq__(other)

  ##########################################
  # Check methods
  ##########################################

  def checkHasDatum (self, datum_id, nature=None):
    u"""
    Checks if book contain a 'datum_id' datum and if this datum has the good 
    nature. If nature=None, it doesn't check nature.
    
    :param datum_id: datum identifier
    :type datum_id: unicode
    :param nature: datum nature
    :type nature: type
    """
    check_type(u'datum_id', datum_id, unicode)
    check_type(u'nature', nature, type, none=True)

    if datum_id in self :
      if nature is None or self[datum_id].nature == nature :
        pass
      else :
        err = error(u'E011 - %(DATUM_ID)s must be type of %(NATURE)s, got %(CUR_NATURE)s' % dict(
          DATUM_ID=datum_id,
          NATURE=nature.__name__,
          CUR_NATURE=self[datum_id].nature.__name__))
        raise TypeError(err)
    else :
      err = error(u'E010 - Book must contain key %(DATUM_ID)s. Key not found.' % dict(DATUM_ID=datum_id))
      raise KeyError(err)
  def _checkDatumType (self, datum):
    u"""
    Checks if this datum type is allowed by this book.
    """
    if not isinstance(datum, self._datum_type) :
      raise TypeError, error(u'E012 - Datum class must be %(DATUM_TYPE)s' % dict(
        DATUM_TYPE=self._datum_type.__name__))

  def _checkDatumNature(self, datum):
    u"""
    Checks if datum.nature is valid (if Book.nature is defined)
    
    :param datum: the datum you want to test
    :type datum: :class:`~pylot.core.Datum`
    """
    if self.identity.nature :
      if not issubclass(datum.nature, self.identity.nature) :
        raise TypeError, error(u"type must be %s, (current type is %s)" % (
          self.identity.nature, datum.nature))

  @classmethod
  def _checkDatumName (cls, key, datum):
    u"""
    Checks if key and datum name are identical.
    If doesn't match, a NameError exception is raised.
    """
    try :
      assert (datum.name == key)
    except AssertionError :
      raise NameError, error(u'key (%s) and datum name(%s) must be identical.' % (
        key, datum.name))

  def _getinfos (self):
    u"""
    :return: useful book informations
    :rtype: `unicode`
    """
    w_nature = 8
    w_group = 10
    w_key = 20
    line = u'-' * (w_nature + w_group + w_key + 8) + u'\n'
    ustr = line
    ustr += u'%s|%s|%s|%s\n' % (u'nature'.ljust(w_nature),
                               u'group'.ljust(w_group),
                               u'key'.ljust(w_key), u'value')
    ustr += line
    keys = self.keys()
    keys.sort()
    for key in keys :
      nature = unicode2(self[key].nature.__name__)
      group = unicode2(self[key].group)
      value = unicode2(self[key].value)
      ustr += u'%s|%s|%s|%s\n' % (nature.ljust(w_nature),
                                    group.ljust(w_group),
                                    key.ljust(w_key), value)
    ustr += line
    return ustr.rstrip()

  def _setname(self, name) :
    self.identity.name = name

  def _getname(self):
    return self.identity.name

  def _getnature(self):
    return self.identity.nature

  def _getalias(self):
    return self.identity.alias

  def _gettooltip(self):
    return self.identity.tooltip

  def _settooltip(self, tooltip):
    self.identity.tooltip = tooltip

  tooltip = property(fget=_gettooltip, fset=_settooltip)
  infos = property(fget=_getinfos)
  nature = property(fget=_getnature,
    doc=u"book type (e.g. bool, int, Path,...)")
  name = property(fget=_getname, fset=_setname,
                  doc=u"book name, stored in identity")
  title = name
  alias = property(fget=_getalias, doc=u"book alias, stored in identity")

###########################################
#  Not in specifications
###########################################

  valid_file_format = (u'csv', u'pylot')
  best_file_format = u'csv'

  def upFromDatum (self, datum, mod_attrs=None):
    if mod_attrs is None :
      mod_attrs = datum.getFieldsAll()
    self._up(datum, mod_attrs=mod_attrs)

  def upFromData (self, *t, **d):
    self._up(*t, **d)

  def upFromFile (self, src, dataformat=None, entries=None, mod_attrs=None,
                  only_existing_keys=False) :
    u"""
    Updates book from a file.
    File format is detected from filename extension
    Supported file formats are defined in Book.valid_file_format

    :param dataformat: force file format
    :type dataformat: unicode

    :param src: file path
    :type src: unicode or Path
    """
    # TODO: support only_existing_keys
    check_type(u'src', src, (unicode, Path))
    check_type(u'dataformat', dataformat, unicode, none=True)
    check_type(u'entries', entries, [list, tuple], none=True)
    check_type(u'mod_attrs', mod_attrs, [list, tuple], none=True)

    src = Path(src)
    if dataformat is None :
      if src.ext == u'.csv' :
        dataformat = u'csv'
      elif src.ext == u'.pylot' :
        dataformat = u'pylot'
      else :
        raise ValueError, error(u'Unknown file type %s' % src)
    else :
      dataformat = dataformat.lower()

    assert(dataformat in self.valid_file_format)
    try :
      stream = Utils.readfile(src)
    except (OSError, IOError) :
      pass
    else :
      if dataformat == u'csv' :
        for line in stream :
          line = line.strip()
          if line and not regexpr.co_comment.match(line) :
            datum = self._datum_type.FromCsv(line)
            if entries is None or datum.name in entries :
              if datum :
                self.upgrade({datum.name:datum}, mod_attrs=mod_attrs)

      elif dataformat == u'pylot' :
        for line in stream :
          line = line.strip()
          if line and not regexpr.co_comment.match(line) :
            datum = self._datum_type()
            datum.upFromPyLot(line)
            if entries is None or datum.name in entries :
              if datum :
                self._up(datum, mod_attrs=mod_attrs)

      stream.close()

  def importfile(self, src, fmt):
    self.clear()
    self.upFromFile(src, fmt)

  def _orderKeys(self, alpha_sort):
    u"""
    Returns alphabetically sorted list if alpha_sort=True else returns book.keys

    :type alpha_sort: bool
    :return: key list
    :rtype: `list`
    """
    keys = self.keys()
    if alpha_sort is True :
      keys.sort()
    return keys

  def toStream (self, stream, fmt=best_file_format, indent=u'',
                alpha_sort=False):
    u"""
    Writes representation of book on 'stream'
    
    Current supported formats are :
      - csv : utf-8 CSV format
      - pyLot : ASCII key/value format

    :param stream: the stream where you want to write
    :param format: representation format. If not specified, used best one.
    :type format: unicode
    :param indent: text indentation for datum
    :type indent: unicode
    :param alpha_sort: sort datum alphabetically
    :type alpha_sort: bool
    """
    check_type(u'fmt', fmt, unicode)
    check_type(u'indent', indent, unicode)
    check_type(u'alpha_sort', alpha_sort, bool)

    fmt = fmt.lower()
    assert(fmt in self.valid_file_format)

    if fmt.lower() == u'csv' :
      stream.write(u'%s# %s\n' % (indent, self._datum_type().csv_order))
      for key in self._orderKeys(alpha_sort) :
        stream.write(indent + self[key].toCsv() + u'\n')

    elif fmt.lower() == u'pylot' :
      for key in self._orderKeys(alpha_sort) :
        stream.write(self[key].toPyLot() + u'\n')

    else :
      raise ValueError, error(u'Unknown file format %s' % format)

  def toFile (self, dest, dataformat=best_file_format):
    u"""
    Saves book to file

    :param dest: destination file name
    :type dest: str or Path

    :param format: set file format. Default value : format which keep the most informations
    :type format: str
    """
    check_type(u'dest', dest, (unicode, Path))
    check_type(u'dataformat', dataformat, unicode)
    dataformat = dataformat.lower()
    check_value(u'dataformat', dataformat , [u'csv', u'pylot'])

    if dataformat.lower() == u'csv' :
      openfile = Utils.writefile(dest, u'force')
      openfile.write(u'# pylot.core.Book CSV file -- version : 0.2\n')
      self.toStream(openfile, dataformat)
      openfile.close()

    elif dataformat.lower() == u'pylot' :
      openfile = Utils.bwritefile(dest, u'force')
      openfile.write(u'# pylot.core.Book pyLot file -- version : 0.2\n')
      self.toStream(openfile, dataformat)
      openfile.close()

  def exportfile(self, dest, fmt=u'csv'):
    self.toFile(dest, fmt)

  def groupDict(self):
    u"""
    >>> book = Book()
    >>> d = Datum(u'PATH1', Path, u'/usr/bin', group=u'grap')
    >>> book.upFromDatum(d)
    >>> d = Datum(u'INT', int, 1,group=u'nombre')
    >>> book.upFromDatum(d)
    >>> d = Datum(u'INT_G3', int, 1, group=u'troisiemegroupe')
    >>> book.upFromDatum(d)
    >>> d = Datum(u'PATH2', Path, u'/usr/local', group=u'grap')
    >>> book.upFromDatum(d)
    >>> for d in book.groupDict()[u'grap'] : print d
    Datum(u'PATH1', Path, Path(u'/usr/bin'))
    Datum(u'PATH2', Path, Path(u'/usr/local'))

    :return: a dictionary, each key of which is a d.group, for d in self.values(),
    corresponding item is a list of Datum with group==key
    """
    group_dic = {}
    datum_keys = self.keys()
    datum_keys.sort()
    for datumid in datum_keys:
      datum = self[datumid]
      try : #on essaye d'ajouter datum a la liste des datum de ce groupe
        group_dic[datum.group].append(datum)
      except KeyError:#si on ne peut pas, on crée la cle et la liste
        group_dic[datum.group] = [datum]
    return group_dic

class GroupBook (Book) :
  u"""
  """
  def __init__ (self, *args, **kargs):

    try :
      kargs[u'identity']
    except KeyError :
      kargs[u'identity'] = Identity(nature=bool)
    else :
      kargs[u'identity'].nature = bool

    # TODO: Python3: remove lines from here ...
    new_dict = {}
    for key, value in kargs.items() :
      new_dict[key.encode(ENCODING)] = value
    # ... to here
    super(GroupBook, self).__init__(*args, **new_dict)

  def getMetaInfos (self, group_id):
    u"""
    returns alias, tooltip.
    If alias is None, alias=''
    If tooltip is None, tooltip=''
    """
    try:
      group = self[group_id]
    except KeyError :
      return unicode2(group_id).capitalize(), u''
    else :
      if group.alias is None:
        alias = group_id.capitalize()
      else:
        alias = group.alias
      if group.tooltip is None:
        tooltip = group_id
      else:
        tooltip = u''
      return alias, tooltip

if __name__ == u'__main__' :
  import doctest
  doctest.testmod()

